import MVT from 'ol/format/MVT.js';
import Style from 'ol/style/Style.js';
import Stroke from 'ol/style/Stroke.js';
import { toContext } from 'ol/render.js';
import { get as getProjection } from 'ol/proj.js';
import { VERSION as OL_VERSION } from 'ol/util.js';
import LRUCache from 'ol/structs/LRUCache.js';
import { getForProjection as getTilegridForProjection } from 'ol/tilegrid.js';
import { createFromTemplates as createTileUrlFunctions } from 'ol/tileurlfunction.js';
const format = new MVT();
const styles = [new Style({
        stroke: new Stroke({
            color: 'blue',
            width: 2
        })
    })];
export default class MVTImageryProvider {
    constructor(options) {
        this.urls = options.urls;
        this.ready = true;
        this.readyPromise = Promise.resolve(true);
        this.tileWidth = 256;
        this.tileHeight = 256;
        this.maximumLevel = options.maximumLevel || 20;
        this.minimumLevel = options.minimumLevel || 0;
        this.tilingScheme = new Cesium.WebMercatorTilingScheme;
        this.rectangle = options.rectangle || this.tilingScheme.rectangle;
        this.errorEvent = new Cesium.Event();
        this.credit = options.credit;
        this.hasAlphaChannel = true;
        this.styleFunction_ = options.styleFunction || (() => styles);
        this.projection_ = getProjection('EPSG:3857');
        this.emptyCanvas_ = document.createElement('canvas');
        this.emptyCanvas_.width = 1;
        this.emptyCanvas_.height = 1;
        this.tileRectangle_ = new Cesium.Rectangle();
        const cacheSize = options.cacheSize !== undefined ? options.cacheSize : 50;
        this.tileCache = new LRUCache(cacheSize);
        this.featureCache = options.featureCache || new LRUCache(cacheSize);
        // to avoid too frequent cache grooming we allow x2 capacity
        const tileGrid = getTilegridForProjection(this.projection_);
        this.tileFunction_ = createTileUrlFunctions(this.urls, tileGrid);
    }
    getTileCredits() {
        return [];
    }
    pickFeatures() {
    }
    getTileFeatures(z, x, y) {
        const cacheKey = this.getCacheKey_(z, x, y);
        let promise;
        if (this.featureCache.containsKey(cacheKey)) {
            promise = this.featureCache.get(cacheKey);
        }
        if (!promise) {
            const url = this.getUrl_(z, x, y);
            promise = fetch(url)
                .then(r => (r.ok ? r : Promise.reject(r)))
                .then(r => r.arrayBuffer())
                .then(buffer => this.readFeaturesFromBuffer(buffer));
            this.featureCache.set(cacheKey, promise);
            if (this.featureCache.getCount() > 2 * this.featureCache.highWaterMark) {
                while (this.featureCache.canExpireCache()) {
                    this.featureCache.pop();
                }
            }
        }
        return promise;
    }
    readFeaturesFromBuffer(buffer) {
        let options;
        if (OL_VERSION <= '6.4.4') {
            // See https://github.com/openlayers/openlayers/pull/11540
            options = {
                extent: [0, 0, 4096, 4096],
                dataProjection: format.dataProjection,
                featureProjection: format.dataProjection
            };
        }
        const features = format.readFeatures(buffer, options);
        const scaleFactor = this.tileWidth / 4096;
        features.forEach((f) => {
            const flatCoordinates = f.getFlatCoordinates();
            let flip = false;
            for (let i = 0; i < flatCoordinates.length; ++i) {
                flatCoordinates[i] *= scaleFactor;
                if (flip) {
                    // FIXME: why do we need this now?
                    flatCoordinates[i] = this.tileWidth - flatCoordinates[i];
                }
                if (OL_VERSION <= '6.4.4') {
                    flip = !flip;
                }
            }
        });
        return features;
    }
    getUrl_(z, x, y) {
        const url = this.tileFunction_([z, x, y]);
        return url;
    }
    getCacheKey_(z, x, y) {
        return `${z}_${x}_${y}`;
    }
    requestImage(x, y, z, request) {
        if (z < this.minimumLevel) {
            return this.emptyCanvas_;
        }
        try {
            const cacheKey = this.getCacheKey_(z, x, y);
            let promise;
            if (this.tileCache.containsKey(cacheKey)) {
                promise = this.tileCache.get(cacheKey);
            }
            if (!promise) {
                promise = this.getTileFeatures(z, x, y)
                    .then((features) => {
                    // FIXME: here we suppose the 2D projection is in meters
                    this.tilingScheme.tileXYToNativeRectangle(x, y, z, this.tileRectangle_);
                    const resolution = (this.tileRectangle_.east - this.tileRectangle_.west) / this.tileWidth;
                    return this.rasterizeFeatures(features, this.styleFunction_, resolution);
                });
                this.tileCache.set(cacheKey, promise);
                if (this.tileCache.getCount() > 2 * this.tileCache.highWaterMark) {
                    while (this.tileCache.canExpireCache()) {
                        this.tileCache.pop();
                    }
                }
            }
            return promise;
        }
        catch (e) {
            console.trace(e);
            this.raiseEvent('could not render pbf to tile', e);
        }
    }
    rasterizeFeatures(features, styleFunction, resolution) {
        const canvas = document.createElement('canvas');
        const vectorContext = toContext(canvas.getContext('2d'), { size: [this.tileWidth, this.tileHeight] });
        features.forEach((f) => {
            const styles = styleFunction(f, resolution);
            if (styles) {
                styles.forEach((style) => {
                    vectorContext.setStyle(style);
                    vectorContext.drawGeometry(f);
                });
            }
        });
        return canvas;
    }
}
